﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ELValidation = Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
using Sinacor.Infra.Service.ExceptionHandling;
using System.Reflection;
using System.Diagnostics;
using Sinacor.Infra.Common.Validation;
using Sinacor.Infra.Common.Exceptions;

namespace Sinacor.Infra.Service.Validation
{
    /// <summary>
    /// Classe usada para executar as validações.
    /// </summary>
    public sealed class SinacorValidation
    {

        /// <summary>
        /// Constante com mensagem de erro para número de parâmetros inválidos
        /// </summary>
        private const string INVALID_NUMBER_OF_PARAMETERS = "Invalid number of parameters";

        /// <summary>
        /// Lista de erros de validação encontrados. Esta lista é preenchida conforme as validações
        /// são adicionadas, e é usada pelo método Validade para gerar as devidas exceções de validação.
        /// </summary>
        private List<KeyValuePair<int, ELValidation.ValidationResult>> _validationErrors;


        /// <summary>
        /// Construtor da classe. Inicializa a lista de erros de validação.
        /// </summary>
        public SinacorValidation()
        {
            _validationErrors = new List<KeyValuePair<int, ELValidation.ValidationResult>>();
        }

        /// <summary>
        /// Verifica se o tipo passado como parâmetro herda da classe base SinacorBaseValidatorAttribute
        /// </summary>
        /// <param name="tipo">Tipo a ser verificado</param>
        /// <returns>true, se herda da classe SinacorBaseValidatorAttribute, false, caso contrário</returns>
        private bool ImplementSinacorBaseValidatorAttribute(Type tipo)
        {
            if (tipo.BaseType == typeof(SinacorBaseValidatorAttribute))
                return true;
            else
                return false;
        }

        /// <summary>
        /// Método para verificar se determinado tipo (Type) implementa a interface IParentObjectRequiredToValidate
        /// </summary>
        /// <param name="tipo">Tipo a ser verificado</param>
        /// <returns>Boolean indicando se o tipo implementa ou não IParentObjectRequiredToValidate</returns>
        private bool ImplementIParentObjectRequiredToValidate(Type tipo)
        {
            bool implement = false;
            foreach (Type t in tipo.GetInterfaces())
            {
                if (t == typeof(IParentObjectRequiredToValidate))
                {
                    implement = true;
                    break;
                }
            }
            return implement;
        }

        
        /// <summary>
        /// Método para adicionar uma validação. Este método não recebe Ruleset, e deve ser usado para 
        /// os casos em que o Ruleset não será aplicado. A validação ocorrerá para todos os atributos
        /// de validação que não possuem Ruleset definido.
        /// </summary>
        /// <typeparam name="T">Tipo da entidade de request</typeparam>
        /// <param name="objectToValidate">Objeto que é a entidade de request a ser validada.</param>
        public void AddValidation<T>(T objectToValidate)
        {
            AddValidation<T>(objectToValidate, String.Empty);
        }

        /// <summary>
        /// Método para adicionar uma validação. Este método recebe um Ruleset, e a validação será
        /// aplicada somente para atributos de validação que possuam este Ruleset.
        /// </summary>
        /// <typeparam name="T">Tipo da entidade de request.</typeparam>
        /// <param name="objectToValidate">Objeto que é a entidade de request a ser validada.</param>
        /// <param name="objectToValidate">Ruseset para o qual a validação deve ocorrer.</param>
        public void AddValidation<T>(T objectToValidate, string ruleset)
        {
            Type tipoT = typeof(T);
            PropertyInfo[] properties = tipoT.GetProperties();
            object[] attributes;
            ELValidation.Validator validator = null;

            foreach (PropertyInfo p in properties)
            {
                attributes = p.GetCustomAttributes(false);
                foreach (object atrib in attributes)
                {
                    if (ImplementSinacorBaseValidatorAttribute(atrib.GetType()))
                    {
                        SinacorBaseValidatorAttribute sinacorBaseAtrib = (SinacorBaseValidatorAttribute)atrib;
                        if (sinacorBaseAtrib.Ruleset == ruleset)
                        {
                            validator = sinacorBaseAtrib.CreateValidator();
                            if (validator != null)
                            {
                                validator.MessageTemplate = sinacorBaseAtrib.MessageTemplate;
                                
                                // Se a interface IParentObjectRequiredToValidate é implementada no validator,
                                // ele precisa da referência para objectToValidate, e ela deve ser setada 
                                if (ImplementIParentObjectRequiredToValidate(validator.GetType()))
                                {
                                    ((IParentObjectRequiredToValidate)validator).parentObject = objectToValidate;
                                }

                                // Se for para ignorar nulo, utilizar wrapper
                                if (sinacorBaseAtrib.IgnoreNulls)
                                    validator = new NullIgnoringValidatorWrapper(validator);

                                // Validar e adicionar o codigo do erro e o resultado a lista
                                var errors = validator.Validate(p.GetValue(objectToValidate, null));
                                foreach (var error in errors)
                                    _validationErrors.Add(new KeyValuePair<int, ELValidation.ValidationResult>(sinacorBaseAtrib.CodException, error));
                            }
                        }
                    }
                }
            }
        }


        /// <summary>
        /// Método para executar a validação. Havendo um ou mais casos onde a validação falhe,
        /// será gerada uma exceção, com uma lista de erros contendo os casos onde a 
        /// validação não ocorreu.
        /// </summary>
        public void Validate()
        {
            // Extrair os ValidationResults
            var validationResults = new ELValidation.ValidationResults();
            validationResults.AddAllResults(_validationErrors.Select(x => x.Value));

            if (!validationResults.IsValid)
            {
                ValidationException exceptionValidation;

                // Se em algum dos validators o MessageTemplate foi preenchido
                // contenar todas as menssagens e repassar para a exception
                if (validationResults.Any(x => !string.IsNullOrEmpty(x.Message)))
                {
                    var messages = new StringBuilder();
                    foreach (var result in validationResults)
                        if (!string.IsNullOrEmpty(result.Message))
                            messages.AppendLine(result.Message);

                    exceptionValidation = new ValidationException(messages.ToString(), null);
                }
                else
                    exceptionValidation = new ValidationException();

                // Adicionar o codigo dos erros
                foreach (var error in _validationErrors)
                    exceptionValidation.AddError(error.Key);

                throw exceptionValidation;
            }
        }

        /// <summary>
        /// ATENÇÃO: usa este método quando houver apenas um Request. NÃO é necessário adicionar 
        /// o Request antes, com o AddValidation.
        /// Método para executar a validação. Havendo um ou mais casos onde a validação falhe,
        /// será gerada uma exceção, com uma lista de erros contendo os casos onde a 
        /// validação não ocorreu.
        /// </summary>
        /// <typeparam name="T">Tipo da entidade de request.</typeparam>
        /// <param name="objectToValidate">Objeto que é a entidade de request a ser validada.</param>
        public void Validate<T>(T objectToValidate)
        {
            AddValidation<T>(objectToValidate);
            Validate();
        }

        /// <summary>
        /// ATENÇÃO: usa este método quando houver apenas um Request. NÃO é necessário adicionar 
        /// o Request antes, com o AddValidation.
        /// Método para executar a validação. Havendo um ou mais casos onde a validação falhe,
        /// será gerada uma exceção, com uma lista de erros contendo os casos onde a 
        /// validação não ocorreu.
        /// </summary>
        /// <typeparam name="T">Tipo da entidade de request.</typeparam>
        /// <param name="objectToValidate">Objeto que é a entidade de request a ser validada.</param>
        /// <param name="ruleSet">Ruseset para o qual a validação deve ocorrer.</param>
        public void Validate<T>(T objectToValidate, string ruleSet)
        {
            AddValidation<T>(objectToValidate, ruleSet);
            Validate();
        }
    }
}
